// windows 
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <objbase.h>
#include <olectl.h>
#include <comcat.h>
#include <atlconv.h>
#include <atlbase.h>

// OPC
#include "opcda.h"
#include "opcda_i.c"

// STL and boost
#include <exception>
#include <vector>
#include <string>
#include <iostream>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>

// Data interface
#include "../VestaOPC/DataInterfaceTypeInfo_h.h"    // use your own path here
//#include "../VestaOPC/DataInterfaceTypeInfo_i.c"  // use your own path here

#include "VestaOPCLink.h"

using namespace std;
using namespace boost::algorithm;
using std::vector;
using std::wstring;
using std::cout;
using std::endl;


//============================================================================
class generic_callback : public IOPCDataCallback {
public:

	generic_callback() 
	{
		m_ulRefs = 1;
	}
    
	//==========================================================================
    // IUnknown

	// QueryInterface
	STDMETHODIMP QueryInterface(REFIID iid, LPVOID* ppInterface) 
	{
		if (ppInterface == NULL)
		{
			return E_INVALIDARG;
		}

		if (iid == IID_IUnknown)
		{
			*ppInterface = dynamic_cast<IUnknown*>(this);
			AddRef();
			return S_OK;
		}

		if (iid == IID_IOPCDataCallback)
		{
			*ppInterface = dynamic_cast<IOPCDataCallback*>(this);
			AddRef();
			return S_OK;
		}

		return E_NOINTERFACE;
	}

	// AddRef
	STDMETHODIMP_(ULONG) AddRef()
	{
        return InterlockedIncrement((LONG*)&m_ulRefs); 
	}

	// Release
	STDMETHODIMP_(ULONG) Release()
	{
        ULONG ulRefs = InterlockedDecrement((LONG*)&m_ulRefs); 

        if (ulRefs == 0) 
        { 
            delete this; 
            return 0; 
        } 

        return ulRefs; 
	}

	//==========================================================================
    // IOPCDataCallback

    // OnDataChange
    STDMETHODIMP OnDataChange(
        DWORD       dwTransid, 
        OPCHANDLE   hGroup, 
        HRESULT     hrMasterquality,
        HRESULT     hrMastererror,
        DWORD       dwCount, 
        OPCHANDLE * phClientItems, 
        VARIANT   * pvValues, 
        WORD      * pwQualities,
        FILETIME  * pftTimeStamps,
        HRESULT   * pErrors
    )
	{
		USES_CONVERSION;

		for (DWORD ii = 0; ii < dwCount; ii++)
		{
			VARIANT vValue;
			VariantInit(&vValue);

			if (SUCCEEDED(VariantChangeType(&vValue, &(pvValues[ii]), NULL, VT_BSTR)))
			{
				_tprintf(_T("Handle = '%d', Value = '%s'\r\n"), phClientItems[ii], OLE2T(vValue.bstrVal));
				VariantClear(&vValue);
			}
		}

		return S_OK;
	}

    // OnReadComplete
    STDMETHODIMP OnReadComplete(
        DWORD       dwTransid, 
        OPCHANDLE   hGroup, 
        HRESULT     hrMasterquality,
        HRESULT     hrMastererror,
        DWORD       dwCount, 
        OPCHANDLE * phClientItems, 
        VARIANT   * pvValues, 
        WORD      * pwQualities,
        FILETIME  * pftTimeStamps,
        HRESULT   * pErrors
    )
	{
		return S_OK;
	}

    // OnWriteComplete
    STDMETHODIMP OnWriteComplete(
        DWORD       dwTransid, 
        OPCHANDLE   hGroup, 
        HRESULT     hrMastererr, 
        DWORD       dwCount, 
        OPCHANDLE * pClienthandles, 
        HRESULT   * pErrors
    )
	{
		return S_OK;
	}


    // OnCancelComplete
    STDMETHODIMP OnCancelComplete(
        DWORD       dwTransid, 
        OPCHANDLE   hGroup
    )
	{
		return S_OK;
	}

private:

	ULONG m_ulRefs;
};


class scoring_callback : public generic_callback 
{
public:
	scoring_callback(const vector<tag_config_t>& configuration) 
		: timestamps(configuration.size())
		, mean_update_period(configuration.size(), -1)
	{	
	}
    // OnDataChange
    STDMETHODIMP OnDataChange(
        DWORD       dwTransid, 
        OPCHANDLE   hGroup, 
        HRESULT     hrMasterquality,
        HRESULT     hrMastererror,
        DWORD       dwCount, 
        OPCHANDLE * phClientItems, 
        VARIANT   * pvValues, 
        WORD      * pwQualities,
        FILETIME  * pftTimeStamps,
        HRESULT   * pErrors
    )
	{
		//USES_CONVERSION;
		for (DWORD index = 0; index < dwCount; index++)
		{
			unsigned client_handle = phClientItems[index];
			bool is_empty = timestamps[client_handle].dwHighDateTime == 0 &&
							timestamps[client_handle].dwLowDateTime == 0;
			if (!is_empty) {
				unsigned __int64 curr = 
					(static_cast<unsigned __int64>(pftTimeStamps[index].dwHighDateTime) << 32) + 
					pftTimeStamps[index].dwLowDateTime;
				unsigned __int64 prev = 
					(static_cast<unsigned __int64>(timestamps[client_handle].dwHighDateTime) << 32) + 
										timestamps[client_handle].dwLowDateTime;
				double ms_dif = 1.0 * (curr - prev) / 10000.0;
								
				mean_update_period[client_handle] = ms_dif;
				/*if (mean_update_period[client_handle] > 0) {
					mean_update_period[client_handle] = 
						0.7 * mean_update_period[client_handle] +
						0.3 * ms_dif;
				}
				else {
					mean_update_period[client_handle] = ms_dif;
				}*/
			}
			timestamps[client_handle] = pftTimeStamps[index];

			//pftTimeStamps

			/*VARIANT vValue;
			VariantInit(&vValue);
			if (SUCCEEDED(VariantChangeType(&vValue, &(pvValues[ii]), NULL, VT_BSTR)))
			{
				_tprintf(_T("Handle = '%d', Value = '%s'\r\n"), phClientItems[ii], OLE2T(vValue.bstrVal));
				VariantClear(&vValue);
			}*/
		}
		unsigned counter = 0;
		for (unsigned index = 0; index < mean_update_period.size(); ++index) {
			if (mean_update_period[index] > 0) {
				++counter;
			}
		}
		/*double sum = 0;
		for (unsigned index = 0; index < mean_update_period.size(); ++index) {
			if (mean_update_period[index] > 0)
				sum += mean_update_period[index] / counter;
		}*/
		if (counter > 0) 
		{
			unsigned __int64 last = (static_cast<unsigned __int64>(timestamps[0].dwHighDateTime) << 32) + 
							timestamps[0].dwLowDateTime;
			cout << mean_update_period[0] << ", counter " << counter;
			cout << ", " << (last / 10 / 1000) << "ms" << endl;
		}

		

		return S_OK;
	}
private:
	vector<FILETIME> timestamps;
	vector<double> mean_update_period;
};

class opc_advise_client {

private:
	CComPtr<IOPCServer> ipServer;
	CComPtr<IOPCItemMgt> ipGroup;
	CComPtr<IConnectionPoint> ipCP;
	OPCHANDLE hGroup;
	DWORD dwAdvise;

public:
	opc_advise_client(LPTSTR szProgID, IOPCDataCallback* callback, DWORD update_rate) {
		// lookup CLSID from ProgID.
		try {
			CLSID cClsid = GUID_NULL;
			if (FAILED(CLSIDFromProgID(T2OLE(szProgID), &cClsid))) {
				if (UuidFromString((RPC_WSTR)szProgID, &cClsid) != RPC_S_OK)
					throw std::exception("Could not resolve Prog ID/CLSID");
			}
			connect(cClsid);
			add_group(callback, update_rate);
		}
		catch (std::exception& e) {
			throw e;
		}
	}
	~opc_advise_client () {
		if (ipCP != NULL)
			ipCP->Unadvise(dwAdvise);
		if (ipServer != NULL)
			ipServer->RemoveGroup(hGroup, FALSE);

		ipCP.Release();
		ipGroup.Release();
		ipServer.Release();
	}
	void add_item(const LPTSTR item_string) {
		vector<wstring> item_strings(1, wstring(item_string));
		add_items(item_strings);
	}
	void add_items(vector<wstring>& item_strings) {
		DWORD          dwCount  = item_strings.size();

		// add items to group on server.
		vector<OPCITEMDEF> pItems(item_strings.size());
		for (unsigned index = 0; index < item_strings.size(); ++index) {
			pItems[index].szItemID            = &item_strings[index][0];
			pItems[index].szAccessPath        = NULL;
			pItems[index].bActive             = TRUE;
			pItems[index].hClient             = index;
			pItems[index].vtRequestedDataType = VT_EMPTY;
			pItems[index].dwBlobSize          = 0;
			pItems[index].pBlob               = NULL;
		}
		OPCITEMRESULT* pResults = NULL;
		HRESULT*       pErrors  = NULL;
		HRESULT hResult = ipGroup->AddItems(pItems.size(), &pItems.front(), &pResults, &pErrors);
		if (FAILED(hResult) || hResult == S_FALSE) {
			throw std::exception("AddItems failed.");
		}
		else {
			// must always free the blob that the server may return.
			for (DWORD ii = 0; ii < dwCount; ii++) {
				if (pResults[ii].dwBlobSize > 0) {
					CoTaskMemFree(pResults[ii].pBlob);
				}
			}
			// free other memory.
			CoTaskMemFree(pResults);
			CoTaskMemFree(pErrors);
		}
	}
private:
	void connect(const CLSID& cClsid) {
		// initialize server security info.
		COSERVERINFO cInfo; 
		memset(&cInfo, 0, sizeof(cInfo));

		cInfo.pwszName    = NULL;//T2OLE(szHostName);
		cInfo.pAuthInfo   = NULL;
		cInfo.dwReserved1 = NULL;
		cInfo.dwReserved2 = NULL;

		// setup requested interfaces.
		MULTI_QI cResults;
		memset(&cResults, 0, sizeof(cResults));

		cResults.pIID = &IID_IOPCServer;
		cResults.pItf = NULL;
		cResults.hr   = S_OK;

		// call create instance.
		HRESULT hResult = CoCreateInstanceEx(
			cClsid,
			NULL,
			CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER,
			&cInfo,
			1,
			&cResults);

		if (FAILED(hResult))
			throw std::exception("CoCreateInstanceEx failed");

		// check that interface is supported.
		if (FAILED(cResults.hr))
			throw std::exception("QueryInterface failed.\r\n");
		ipServer = (IOPCServer*)cResults.pItf;
	}
	void add_group(IOPCDataCallback* callback, DWORD update_rate) {
		HRESULT hResult;

		// add group to server.
		DWORD dwRevisedUpdateRate = 0;
		hResult = ipServer->AddGroup(
			L"",
			TRUE,
			update_rate, 
			NULL,
			NULL,
			NULL,
			LOCALE_SYSTEM_DEFAULT,
			&hGroup,
			&dwRevisedUpdateRate,
			IID_IOPCItemMgt,
			(IUnknown**)&ipGroup
		);

		if (FAILED(hResult))
			throw std::exception("AddGroup failed."); 

		// set up connection point.
		CComPtr<IConnectionPointContainer> ipCPC;
		hResult = ipGroup->QueryInterface(IID_IConnectionPointContainer, (void**)&ipCPC);
		if (FAILED(hResult)) {
			ipServer->RemoveGroup(hGroup, FALSE);
			throw std::exception("QueryInterface for IConnectionPointContainer failed.");
		}

		hResult = ipCPC->FindConnectionPoint(IID_IOPCDataCallback, &ipCP);
		if (FAILED(hResult)) {
			ipServer->RemoveGroup(hGroup, FALSE);
			throw std::exception("QueryInterface for IConnectionPointContainer failed.");
		}

		// create the callback object.
		hResult = ipCP->Advise(callback, &dwAdvise);
		if (FAILED(hResult)) {
			ipServer->RemoveGroup(hGroup, FALSE);
			throw std::exception("Advise failed.");
		}

	}

};

void OpenAndConfigureGate(void** context, vector<tag_config_t>* configuration) {

	// Configure
	configuration->resize(10);
	vector<wstring> paths(configuration->size());
	for (unsigned index = 0; index < configuration->size(); ++index) {
		tag_config_t& tag_config = configuration->at(index);
		tag_config.tag_id = index;
		tag_config.opc_access = ReadOnly;
		tag_config.vt = VT_R8;
		
		wstringstream path_stream;
		path_stream << L"object/value" << index;
		paths[index] = path_stream.str();
		tag_config.opc_path = const_cast<wchar_t *>(paths[index].c_str());
	}

	cout << "OpenDataConnection()" << endl;
	*context = OpenDataConnection();
	if (!*context)
		return;
	cout << "configure()... " << endl;
	ConfigureOPCInterface(*context, &configuration->front(), configuration->size());
	cout << "OK" << endl;

}

void WriteDataToGate(void* context, const vector<tag_config_t>& configuration) {
	//connection_context_t* connection_context = 
	//	reinterpret_cast<connection_context_t *> (context);	// Write

	vector<tag_value_t> values(configuration.size());
	for (unsigned index = 0; index < values.size(); ++index) {
		tag_value_t& tag_value = values[index];
		tag_value.tag_id = configuration[index].tag_id;
		tag_value.value.vt = configuration[index].vt;
		tag_value.value.dblVal = 332.2 + index;
		//tag_value.value.intVal = 332.2 + index;
	}
	cout << "writing ";
	for (unsigned counter = 0; counter < 100; ++counter)  
	{
		for (unsigned index = 0; index < values.size(); ++index) {
			tag_value_t& tag_value = values[index];
			tag_value.value.dblVal++;
		}

		WriteData(context, values.size(), &values.front());
		
		Sleep(2000);
		//cout << ".";
	}
}


int _tmain(int argc, _TCHAR* argv[])
{
	if (FAILED(CoInitializeEx(NULL, COINIT_MULTITHREADED)))
		throw std::exception("CoInitialize failed");
	
	try {
		// 1. Open data interface
		void* context;
		vector<tag_config_t> configuration;
		OpenAndConfigureGate(&context, &configuration);

		// 2. Open OPC connection
		scoring_callback callback(configuration);
		opc_advise_client client(L"VestaOPC.OpcDa20Server.1", (IOPCDataCallback *) &callback, 100);
		
		vector<wstring> var_paths(configuration.size());
		for (unsigned index = 0; index < configuration.size(); ++index) {
			wstringstream path_stream; 
			path_stream << L"object/value" << index;
			var_paths[index] = path_stream.str().c_str();
		}
		client.add_items(var_paths);

		// 3. Write data and estimate speed with callback
		WriteDataToGate(context, configuration);
		Sleep(10000);

		// 4. Close data interface
		CloseDataConnection(context);
	}
	catch (std::exception& e) {
		cout << e.what() << endl;
	}

	CoUninitialize();

	return 0;
}

